/*

This file is part of Rooms3D.

Copyright (C) 2008 Pixecur Technologies Incorporated (Esotera).
Visit Esotera at http://www.esoteras3.com
 
Rooms3D is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3, or (at your option)
any later version.
 
Rooms3D is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
General Public License for more details.
 
You should have received a copy of the GNU General Public License
along with Rooms3D; see the file COPYING.  If not, write to 
Pixecur Technologies, 213-31 Peet Street, St. John's, NL, A1B 3W8. 
 
Linking this library statically or dynamically with other modules is
making a combined work based on this library.  Thus, the terms and
conditions of the GNU General Public License cover the whole
combination.
 
 */
package com.datasentinel.rooms.ui.sh3d;

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Window;
import java.awt.event.HierarchyBoundsListener;
import java.awt.event.HierarchyEvent;
import java.awt.geom.GeneralPath;
import java.awt.geom.Line2D;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.Adler32;
import java.util.zip.CheckedInputStream;
import java.util.zip.CheckedOutputStream;

import javax.media.j3d.Transform3D;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import javax.vecmath.AxisAngle4f;
import javax.vecmath.Point3f;
import javax.vecmath.Vector3f;

import com.datasentinel.file.FileBuilder;
import com.datasentinel.file.FileBuilderFactory;
import com.datasentinel.rooms.file.FileConstants;
import com.datasentinel.rooms.file.FolderMonitor;
import com.datasentinel.rooms.file.sh3d.FilePieceOfFurnitureTextureManager;
import com.datasentinel.rooms.file.sh3d.HomeFilePieceOfFurniture;
import com.datasentinel.rooms.ui.sh3d.roomeditor.CustomHomeFileRecorder;
import com.datasentinel.util.VersionInfo;
import com.datasentinel.util.ui.gesture.GestureListener;
import com.eteks.sweethome3d.io.DefaultUserPreferences;
import com.eteks.sweethome3d.model.Camera;
import com.eteks.sweethome3d.model.Home;
import com.eteks.sweethome3d.model.HomePieceOfFurniture;
import com.eteks.sweethome3d.model.PieceOfFurniture;
import com.eteks.sweethome3d.model.RecorderException;
import com.eteks.sweethome3d.model.UserPreferences;
import com.eteks.sweethome3d.model.Wall;

/**
 * @author rickp
 * 
 */
public class Rooms3DController
{
	private static Logger logger = Logger.getLogger(Rooms3DController.class.getName());
	private Container contentPane;
	private static CustomHomeFileRecorder homeRecorder = new CustomHomeFileRecorder();
	private UserPreferences preferences = new DefaultUserPreferences();
	private CustomHomeController3D homeController3D = null;
	private CustomHomeComponent3D home3D = null;
	private Home home = null;
	private DoorCollisionListener doorCollisionListener;
	private File roomFolder;
	private File roomFile;
	private static FileBuilder fileBuilder = FileBuilderFactory.createFileBuilder();
	private List<GestureListener> gestureListeners;
	private Thread homeSaverThread;
	private HomeSaver homeSaver;
	private static long lastHomeChecksum = -1L;
	private Stack<File> history = new Stack<File>();
	private File lastPopped;
	private static ImageIcon image = new ImageIcon(FilePieceOfFurnitureTextureManager.class
			.getResource("bigrotation2.gif"));
	private static Thread loadHomeThread = null;
	private static final Object loadHomeLock = new Object();
	private static final Object saveHomeLock = new Object();
	
	private static final VersionInfo versionInfo = new VersionInfo("version.properties");

	public Rooms3DController(Container container)
	{
		logger.info("initializing Rooms3D: " + versionInfo.getVersionInformation());
		
		if("\\".equals(File.separator))
		{
			logger.info("Configuring Java3D for DirectX 3D Pipeline");
			System.setProperty("j3d.rend","d3d");
		}
		
		if (container == null)
		{
			container = new JPanel();
			container.setPreferredSize(new Dimension(800, 600));
			container.setLayout(new BorderLayout());
		}
		this.contentPane = container;
		gestureListeners = Collections.synchronizedList(new ArrayList<GestureListener>());
		doorCollisionListener = new DoorCollisionListener() {
			private long lastCollideTime = -1L;

			@Override
			public void collidedWith(PieceOfFurniture door)
			{
				if (-1L != lastCollideTime && (System.currentTimeMillis() - lastCollideTime) < 500)
				{
					// only need the first of the series
					return;
				}
				lastCollideTime = System.currentTimeMillis();
				System.err.println("collided with door");
				try
				{
					lastPopped = history.empty()?fileBuilder.fileHome():history.pop();
					home3D.fireActivationEvent(lastPopped);
				}
				catch (IOException e)
				{
					logger.log(Level.SEVERE, "Cannot determine home file", e);
				}
			}
		};
		hierarchyBoundsListener = new HierarchyBoundsListener(){
			@Override
			public void ancestorMoved(HierarchyEvent e)
			{
			}
			@Override
			public void ancestorResized(HierarchyEvent e)
			{
				if (redrawAction != null)
					if (redrawActionThread == null || !redrawActionThread.isAlive())
						(redrawActionThread = new Thread(redrawAction)).start();
					else
						redrawAction.postpone();
			}};
	}
	private Thread redrawActionThread;
	private DelayedAction redrawAction;
	
	public Rooms3DController()
	{
		this(null);
	}

	class CameraPosition
	{
		private float pitch;
		private float yaw;
		private float x;
		private float y;
		private float z;
		
		public CameraPosition(Home home)
		{
			Camera camera = home.getCamera();
			pitch = camera.getPitch();
			yaw = camera.getYaw();
			x = camera.getX();
			y = camera.getY();
			z = camera.getZ();
		}
		
		public void set(Home home)
		{
			home.setCameraLocation(home.getCamera(), x, y, z);
			home.setCameraAngles(home.getCamera(), yaw, pitch);
		}
	}

	synchronized File saveHome() throws RecorderException
	{
		if (null != home) // && (null == homeSaverThread || !homeSaverThread.isAlive()))
		{
			printTime("before HomeSaver()");
			homeSaver = new HomeSaver(home, roomFile);
			printTime("after HomeSaver()");
			homeSaverThread = new Thread(homeSaver, String.format("HomeSaver [%s]", roomFile));
			homeSaverThread.start();
		}
		return roomFile;
	}

	private File getRoomFile(File folder) throws IOException
	{
		printTime("before metaDirectory = fileBuilder.build()");
		File metaDirectory = fileBuilder.build(folder, FileConstants.ROOMS3D_FOLDER);
		printTime("before metaDirectory.exists()");
		if (!metaDirectory.exists())
		{
			printTime("before metaDirectory.mkdirs()");
			metaDirectory.mkdirs();
		}
		printTime("before fileBuilder.build()");
		File roomFile = fileBuilder.build(metaDirectory, FileConstants.ROOMS3D_FILENAME);
		printTime("before roomFile.exists()");
		if (!roomFile.exists() && !roomFile.createNewFile())
			throw new IOException("could not create " + roomFile);
		return roomFile;
	}

	private void checkForStaleOrDuplicateFilePieces()
	{
		try
		{
			// check to make sure we have no stale pieces in our rooms file
			logger.fine("StaleFilePieceCheck.run(): home = " + home + ", home.getFurniture() = "
					+ home.getFurniture());
			HomePieceOfFurniture[] homePieces = home.getFurniture().toArray(
					new HomePieceOfFurniture[0]);
			logger.fine("checking for stale or duplicate file pieces, number to check: " + homePieces.length);
			int numStaleDeleted = 0;
			File root = roomFile.getParentFile().getParentFile();
			ArrayList<File> filesChecked = new ArrayList<File>();
			for (HomePieceOfFurniture piece : homePieces)
			{
				if (piece instanceof HomeFilePieceOfFurniture)
				{
					HomeFilePieceOfFurniture filePiece = (HomeFilePieceOfFurniture)piece;
					File file = filePiece.getFile();
					if (file == null || !fileBuilder.exists(root, file.getName()) || filesChecked.contains(file))
					{
						home.deletePieceOfFurniture(filePiece);
						numStaleDeleted++;
					}
					filesChecked.add(file);
				}
			}
			logger.fine(String.format("stale check complete, %d deleted, %d left",
						numStaleDeleted, home.getFurniture().size()));
		}
		catch (Exception e)
		{
			throw new RuntimeException(e);
		}
	}
	public long timestamp;
	public void printTime(String message)
	{
		logger.fine((System.currentTimeMillis()-timestamp) + "mSec: " + message);
	}
	public Thread loadHome(final String roomName, final File folder) throws RecorderException
	{
		timestamp = System.currentTimeMillis();
		System.err.println("loadHome called for " + roomName);
		synchronized (loadHomeLock)
		{
			if (null != loadHomeThread && loadHomeThread.isAlive())
			{
				try
				{
					printTime("load home in progress... waiting...");				
					loadHomeLock.wait();
				}
				catch (InterruptedException ie)
				{
					return null;
				}
			}
			
			//FolderMonitor.getInstance().cancel();
			
			contentPane.removeAll();
			setContentPane(contentPane);
			printTime("content pane updated");
			loadHomeThread = new Thread() {
				public void run()
				{
					synchronized (loadHomeLock)
					{
						try
						{
							printTime("building roomFile");
							roomFile = getRoomFile(folder);
							printTime("choosing source of roomFile");
							byte homeBytes[] = null;
							InputStream roomIn = null;
							if (null != homeSaverThread && homeSaverThread.isAlive() 
									&& homeSaver.getHomeFile().equals(roomFile) 
									&& null != (homeBytes = homeSaver.getHomeBytes()))
							{
								logger.fine("entering a room while a save for that rooms is in progress");
								logger.fine("current in-progress save will be cancelled.");
								roomIn = new ByteArrayInputStream(homeBytes);
								logger.fine("interrupting "+homeSaverThread);
								homeSaverThread.interrupt();
								loadHome(roomName, roomIn);
								lastHomeChecksum = -1L; // make sure we save the next time
							}
							else
							{
								if (roomFolder != null)
									history.push(roomFolder);
								roomFolder = folder;
								roomIn = (null == roomFile || 0 == roomFile.length())?getClass()
										.getResourceAsStream("defaultRoom.rmz")
										:fileBuilder.buildFileInputStream(roomFile);
								loadHome(roomName, roomIn);
							}
							printTime("loadHome(Stream) completed on loadHome(File) thread");
							checkForStaleOrDuplicateFilePieces();
							printTime("checkForStaleOrDuplicateFilePieces completed");
							FolderMonitor.getInstance().setFolder(folder);
						}
						catch (Throwable e)
						{
							logger.log(Level.SEVERE, "Error loading room", e);
							Thread.currentThread().setName("Room load failed: " + e.getLocalizedMessage());
						}
						finally
						{
							printTime("finally block entered on loadHome(File)");
							loadHomeLock.notify();
							EventQueue.invokeLater(new Runnable() { public void run() {
								contentPane.removeAll();
								contentPane.add(home3D, BorderLayout.CENTER);
							}});
							printTime("loadHome(File) thread completed");
						}
					}
				}
			};
			loadHomeThread.setName("load Home started");
			loadHomeThread.start();
			return loadHomeThread;
		}
	}
	HierarchyBoundsListener hierarchyBoundsListener;
	private void loadHome(String roomName, InputStream istream) throws RecorderException
	{
		CheckedInputStream checkedHomeIn = new CheckedInputStream(istream, new Adler32());
		home = homeRecorder.readHome(checkedHomeIn);
		printTime("homeRecorder.readHome complete");
		try
		{
			checkedHomeIn.close();
		}
		catch (IOException ioe)
		{
			throw new RecorderException(ioe.getMessage());
		}
		printTime("checkedHomeIn closed");
		lastHomeChecksum = checkedHomeIn.getChecksum().getValue();
		home.setName(roomName);
		home.setCamera(home.getObserverCamera());
		positionCameraNextToDoor(home);
		long loadStartTime = System.currentTimeMillis();
		homeController3D = new CustomHomeController3D(home, preferences, null);
		logger.warning("CustomHomeController3D loaded '" + roomName + "'in: "
				+ (System.currentTimeMillis() - loadStartTime) + "ms");
		home3D = (CustomHomeComponent3D)homeController3D.getView();
		logger.log(Level.INFO, "CustomHomeComponent3D.getView() = " + home3D);
		home3D.addAncestorListener(new AncestorListener() {
			@Override
			public void ancestorAdded(AncestorEvent event)
			{
				printTime("ancestorAdded before sync");
				synchronized (gestureListeners)
				{
					logger.log(Level.FINE, "ancestor added start");
					printTime("ancestorAdded inside sync");
					if (home3D != null)
					{
						for (GestureListener listener : gestureListeners)
							home3D.addGestureListener(listener);
						logger.log(Level.FINE, String.format("adding HierarchyBoundsListener on [%s]", home3D));
						contentPane.addHierarchyBoundsListener(hierarchyBoundsListener);
						
						redrawAction = new DelayedAction(new Runnable(){
							public void run(){
								if (contentPane != null && home3D != null)
								{
									CameraPosition cameraPosition = new CameraPosition(home);
									contentPane.removeAll();
									contentPane.add(home3D, BorderLayout.CENTER);
									cameraPosition.set(home);
								}
							}}, 50);
					}
				}
				printTime("ancestorAdded after sync");

				if (homeController3D != null)
					homeController3D.addDoorCollisionListener(doorCollisionListener);
				logger.log(Level.FINE, "ancestor added stop");
			}

			@Override
			public void ancestorMoved(AncestorEvent event)
			{
			}

			@SuppressWarnings("cast")
			@Override
			public void ancestorRemoved(AncestorEvent event)
			{
				try
				{
					printTime("ancestorRemoved");
					logger.log(Level.FINE, "ancestor removed start");
					redrawAction = null;
					// FolderMonitor.getInstance().cancel();
					logger.log(Level.FINE, String.format("removing HierarchyBoundsListener on [%s]", home3D));
					contentPane.removeHierarchyBoundsListener(hierarchyBoundsListener);
					printTime("about to saveHome");
					saveHome();
					printTime("after saveHome");
					if (home3D instanceof CustomHomeComponent3D)
					{
						synchronized (gestureListeners)
						{
							for (GestureListener listener : gestureListeners)
								home3D.removeGestureListener(listener);
						}
						homeController3D.removeDoorCollisionListener(doorCollisionListener);
					}
				}
				catch (RecorderException re)
				{
					throw new RuntimeException(re);
				}
				logger.log(Level.FINE, "ancestor removed end");
			}
		});
	}

	private static void positionCameraNextToDoor(Home home)
	{
		// set camera orientation based on door position
		// will use the first door found
		for (HomePieceOfFurniture homePiece : home.getFurniture().toArray(
				new HomePieceOfFurniture[0]))
		{
			if (homePiece.isDoorOrWindow() && homePiece.getName().toLowerCase().indexOf("door") >= 0)
			{
				float cameraElevation = homePiece.getHeight() * 0.8f;
				home.setCameraAngles(home.getCamera(), homePiece.getAngle(), 0.12f);
				Point3f doorLocation = new Point3f(homePiece.getX(), homePiece.getY(), homePiece
						.getElevation());
				Point3f newCameraLocation = new Point3f(doorLocation.x, doorLocation.y,
						cameraElevation);
				Vector3f xlate = new Vector3f(0, homePiece.getWidth(), 0);
				AxisAngle4f rtate = new AxisAngle4f(0, 0, 1, homePiece.getAngle());
				Transform3D t = new Transform3D();
				t.setRotation(rtate);
				t.transform(xlate);
				newCameraLocation.add(xlate);
				GeneralPath floorArea = new GeneralPath();
				for (Wall wall : home.getWalls())
				{
					Line2D wallLine = new Line2D.Float(wall.getXStart(), wall.getYStart(), wall
							.getXEnd(), wall.getYEnd());
					floorArea.append(wallLine, /* connect to last */true);
				}
				if (!floorArea.contains(newCameraLocation.x, newCameraLocation.y))
				{
					logger.fine("switching camera view...");
					newCameraLocation
							.set(doorLocation.x, doorLocation.y, cameraElevation);
					xlate.set(0, homePiece.getWidth(), 0);
					rtate.set(0, 0, -1, homePiece.getAngle());
					t.setRotation(rtate);
					t.transform(xlate);
					newCameraLocation.add(xlate);
					home.setCameraAngles(home.getCamera(), homePiece.getAngle() - (float)Math.PI,
							0.12f);
				}
				if (floorArea.contains(newCameraLocation.x, newCameraLocation.y))
				{
					home.setCameraLocation(home.getCamera(), newCameraLocation.x,
							newCameraLocation.y, newCameraLocation.z);
				}
				else
					throw new RuntimeException("could not set camera position");
				break;
			}
		}
	}
	
	public void addGestureListener(final GestureListener listener)
	{
		if (!gestureListeners.contains(listener))
			gestureListeners.add(listener);
 	}

	public void removeGestureListener(final GestureListener listener)
	{
		if (gestureListeners.contains(listener))
			gestureListeners.remove(listener);
	}

	public Container getContentPane()
	{
		return contentPane;
	}

	public void setContentPane(final Container contentPane)
	{
		this.contentPane = contentPane;
		EventQueue.invokeLater(new Runnable() {
			public void run()
			{
				contentPane.add(new JLabel(image), BorderLayout.CENTER);
				Window parent = SwingUtilities.getWindowAncestor(contentPane);
				if (parent != null)
					parent.validate();
				//contentPane.validate();
			}
		});
	}

	private static class HomeSaver implements Runnable
	{
		private Home home;
		private File homeFile;
		private ByteArrayOutputStream homeBytes;
		private long checksum;

		HomeSaver(Home home, File homeFile)
		{
			this.home = home;
			this.homeFile = homeFile;
			checksum = lastHomeChecksum;
			homeBytes = prepareForSave();
		}
		
		public byte[] getHomeBytes()
		{
			if (homeBytes != null)
				return homeBytes.toByteArray();
			return null;
		}
		
		public File getHomeFile()
		{
			return homeFile;
		}

		private ByteArrayOutputStream prepareForSave()
		{
			positionCameraNextToDoor(home);
			ByteArrayOutputStream homeBytes = new ByteArrayOutputStream();
			try
			{
				CheckedOutputStream checkedHomeOut = new CheckedOutputStream(homeBytes,
						new Adler32());
				homeRecorder.writeHome(home, checkedHomeOut);
				checkedHomeOut.close();
				long newChecksum = checkedHomeOut.getChecksum().getValue();
				if (newChecksum == checksum)
					return null; // prevent save
				lastHomeChecksum = newChecksum;
			}
			catch (Exception e)
			{
				logger.log(Level.SEVERE, "Error writing home", e);
			}
			return homeBytes;
		}

		private void saveRoomData(ByteArrayOutputStream homeBytes) throws IOException
		{
			logger.fine("changes detected, starting save to '" + homeFile + "'");
			OutputStream homeOut = fileBuilder.buildFileOutputStream(homeFile);
			ByteArrayInputStream homeBytesToSave = new ByteArrayInputStream(homeBytes.toByteArray());
			byte[] buf = new byte[8192];
			int numRead = 0;
			while (-1 != (numRead = homeBytesToSave.read(buf)))
			{
				homeOut.write(buf, 0, numRead);
			}
			homeOut.close();
			logger.fine("changes detected, save completed");
		}

		public void run()
		{
			try
			{
				if (homeBytes != null)
					saveRoomData(homeBytes);
				else
					logger.fine("no changes, skipping save");
			}
			catch (Exception e)
			{
				logger.warning("failure during save: " + e.getMessage());
				throw new RuntimeException(e);
			}
			finally
			{
				homeBytes = null;
				synchronized (saveHomeLock)
				{
					saveHomeLock.notify();
				}
			}
		}
	}
}
